--[[
Name: SpecialEvents-Aura-2.0
Revision: $Rev: 10488 $
Author: Tekkub Stoutwrithe (tekkub@gmail.com)
Website: http://wiki.wowace.com/index.php/SpecialEvents_Addon
Documentation: http://wiki.wowace.com/index.php/SpecialEvents-Aura-2.0
SVN: svn://svn.wowace.com/root/trunk/SpecialEventsEmbed/SpecialEvents-Aura-2.0
Description: Special events for Aura's, (de)buffs gained, lost etc.
Dependencies: AceLibrary, AceEvent-2.0, Gratuity-2.0
]]


local vmajor, vminor = "SpecialEvents-Aura-2.0", "$Revision: 10488 $"

if not AceLibrary then error(vmajor .. " requires AceLibrary.") end
if not AceLibrary:HasInstance("AceEvent-2.0") then error(vmajor .. " requires AceEvent-2.0.") end
if not AceLibrary:IsNewVersion(vmajor, vminor) then return end

local lib = {}
AceLibrary("AceEvent-2.0"):embed(lib)

local gratuity = AceLibrary("Gratuity-2.0")
local RL
local debuffTextureCache = {}
local buffTextureCache = {}


----------------------------
--      Compost Heap      --
----------------------------

local heap = {}
setmetatable(heap, {__mode = "kv"})


local function acquire(a1,a2,a3,a4,a5)
    local t = next(heap)
    if t then
        heap[t] = nil
        assert(not next(t), "A table in the compost heap has been modified!")
    end
    t = t or {}

    if a1 ~= nil then table.insert(t, a1) end
    if a2 ~= nil then table.insert(t, a2) end
    if a3 ~= nil then table.insert(t, a3) end
    if a4 ~= nil then table.insert(t, a4) end
    if a5 ~= nil then table.insert(t, a5) end
    return t
end


local function reclaim(t, d)
    if type(t) ~= "table" then return end

    if d and d > 0 then
        for i in pairs(t) do
            if type(t[i]) == "table" then reclaim(t[i], d - 1) end
        end
    end

    for i in pairs(t) do t[i] = nil end
    t.reset = 1
    t.reset = nil
    table.setn(t, 0)

    heap[t] = true
end


----------------------------
--     Initialization     --
----------------------------

local function registerevents(self)
    self:RegisterEvent("PLAYER_LEAVING_WORLD")
    self:RegisterEvent("UNIT_AURA", "AuraScan")
    self:RegisterEvent("UNIT_AURASTATE", "AuraScan")
    self:RegisterEvent("PLAYER_AURAS_CHANGED")
    self:RegisterEvent("PLAYER_TARGET_CHANGED")
    self:RegisterEvent("PLAYER_REGEN_ENABLED")
    -- check if RosterLib exists. We need to do that here and not earlier.
    RL = AceLibrary:HasInstance("RosterLib-2.0") and AceLibrary("RosterLib-2.0") or nil
    if RL then
        RL:Enable()
        self:RegisterBucketEvent("RosterLib_UnitChanged",0.2)
    else
        self:RegisterEvent("PARTY_MEMBERS_CHANGED",1)
        self:RegisterEvent("RAID_ROSTER_UPDATE",1)
    end
end


-- Activate a new instance of this library
function activate(self, oldLib, oldDeactivate)
    if oldLib then
        self.vars = oldLib.vars
        if oldLib:IsEventRegistered("UNIT_AURA") then registerevents(self) end
        if oldLib:IsEventRegistered("RosterLib_UnitChanged") then
            if RL then RL:Disable() end
            oldLib:UnregisterAllEvents()
        end
    else self.vars = {buffs = {}, debuffs = {}} end

    self:RegisterEvent("PLAYER_ENTERING_WORLD")

    if oldDeactivate then oldDeactivate(oldLib) end
end


function lib:PLAYER_ENTERING_WORLD()
    self:ScanAllAuras()
    registerevents(self)
end


function lib:PLAYER_LEAVING_WORLD()
    self:UnregisterAllEvents()
    self:RegisterEvent("PLAYER_ENTERING_WORLD")
    if RL then
        RL:Disable()
    end
end


--------------------------------
--      Tracking methods      --
--------------------------------

function lib:PLAYER_AURAS_CHANGED()
    self:AuraScan("player")
    if GetNumRaidMembers() > 0 then
        local u
        for i=1,GetNumRaidMembers() do
            if UnitIsUnit("raid"..i, "player") then u = "raid"..i end
        end
        self:AuraScan(u)
    end
end


function lib:PLAYER_TARGET_CHANGED()
    self:AuraScan("target")
    self:TriggerEvent("SpecialEvents_AuraTargetChanged")
end


function lib:RosterLib_UnitChanged(units)
    for unit in pairs(units) do
        if unit and UnitExists(unit) then
            self:AuraScan(unit)
        end
    end
    if GetNumRaidMembers() > 0 then
        self:TriggerEvent("SpecialEvents_AuraRaidRosterUpdate")
    else
        self:TriggerEvent("SpecialEvents_AuraPartyMembersChanged")
    end
end


function lib:PARTY_MEMBERS_CHANGED()
    if UnitExists("pet") then self:AuraScan("pet") end

    for i=1,4 do
        if UnitExists("party"..i) then self:AuraScan("party"..i) end
        if UnitExists("partypet"..i) then self:AuraScan("partypet"..i) end
    end
    self:TriggerEvent("SpecialEvents_AuraPartyMembersChanged")
end


function lib:RAID_ROSTER_UPDATE()
    for i=1,40 do
        if UnitExists("raid"..i) then self:AuraScan("raid"..i) end
        if UnitExists("raidpet"..i) then self:AuraScan("raidpet"..i) end
    end
    self:TriggerEvent("SpecialEvents_AuraRaidRosterUpdate")
end

function lib:PLAYER_REGEN_ENABLED()
    reclaim(debuffTextureCache)
    debuffTextureCache = acquire()
    reclaim(buffTextureCache)
    buffTextureCache = acquire()
end

function lib:ScanAllAuras()
    if UnitExists("player") then self:AuraScan("player") end
    if UnitExists("pet") then self:AuraScan("pet") end

    for i=1,4 do
        if UnitExists("party"..i) then self:AuraScan("party"..i) end
        if UnitExists("partypet"..i) then self:AuraScan("partypet"..i) end
    end

    for i=1,40 do
        if UnitExists("raid"..i) then self:AuraScan("raid"..i) end
        if UnitExists("raidpet"..i) then self:AuraScan("raidpet"..i) end
    end

    if UnitExists("target") then self:AuraScan("target") end
--~~    if UnitExists("mouseover") then self:AuraScan("mouseover") end
end


function lib:AuraScan(targ)
    local maxbuffs, maxdebuffs, t = 16, 16, targ or arg1
    local oldd, oldb = self.vars.debuffs[t], self.vars.buffs[t]
    local newd, newb = acquire(), acquire()

    if t == "player" then
        for i=0,(maxbuffs-1) do
            local bidx = GetPlayerBuff(i, "HELPFUL")
            if bidx and bidx ~= -1 then
                gratuity:SetPlayerBuff(bidx)
                local txt = gratuity:GetLine(1)
                if txt then newb[txt] = i end
            end
        end

        for i=0,(maxdebuffs-1) do
            local didx = GetPlayerBuff(i, "HARMFUL")
            if didx and didx ~= -1 then
                gratuity:SetPlayerBuff(didx)
                local txt, txtr = gratuity:GetLine(1)

                local apps = GetPlayerBuffApplications(didx)
                local texture = GetPlayerBuffTexture(didx)
                local dbtype = GetPlayerBuffDispelType(didx)

                if txt then
                    local txtindex = txt
                    if texture then txtindex = txtindex..texture end
                    newd[txtindex] = acquire(i, txt, apps, dbtype, texture)
                end
            end
        end
    else
        for i=1,maxbuffs do
            local txt
            local texture, apps = UnitBuff (t, i)
            if texture then
                if not buffTextureCache[texture] then
                    gratuity:SetUnitBuff(t,i)
                    txt = gratuity:GetLine(1)
                    buffTextureCache[texture] = txt
                else
                    txt = buffTextureCache[texture]
                end
                if txt then newb[txt] = i end
            end
        end

        for i=1,maxdebuffs do
            local txt
            local texture, apps, dbtype = UnitDebuff (t, i)
            if texture then
                if not debuffTextureCache[texture] then
                    gratuity:SetUnitDebuff(t,i)
                    txt = gratuity:GetLine(1)
                    debuffTextureCache[texture] = txt
                else
                    txt = debuffTextureCache[texture]
                end
                if txt then
                    local txtindex = txt .. texture
                    reclaim(newd[txtindex])
                    newd[txtindex] = acquire(i, txt, apps, dbtype, texture)
                end
            end
        end
    end

    self.vars.buffs[t] = newb
    self.vars.debuffs[t] = newd

    if oldb then
        for b,i in oldb do
            if not newb[b] then
                self:TriggerEvent("SpecialEvents_UnitBuffLost", t, b)
                if t == "player" then self:TriggerEvent("SpecialEvents_PlayerBuffLost", b) end
            end
        end
    end

    if oldd then
        for d,i in pairs(oldd) do
            if type(i) ~= "table" then
                local t = ""
                for d,i in pairs(oldd) do t = t.." "..d.."="..i end
                error(string.format("Debuff error! Unit: %s, Table dump:%s", targ, t))
            elseif not newd[d] then
                self:TriggerEvent("SpecialEvents_UnitDebuffLost", t, i[2], i[3], i[4], i[5])
                if t == "player" then self:TriggerEvent("SpecialEvents_PlayerDebuffLost", i[2], i[3], i[4], i[5]) end
            end
        end
    end

    for b,i in pairs(newb) do
        if (not oldb or not oldb[b]) then
            self:TriggerEvent("SpecialEvents_UnitBuffGained", t, b)
            if t == "player" then self:TriggerEvent("SpecialEvents_PlayerBuffGained", b, i) end
        end
    end

    for d,i in pairs(newd) do
        assert(type(i) == "table", string.format("Debuff: %s - Value not a table: %s", d, type(i) == "table" and "table" or i or "nil"))

        local o2 = oldd and type(oldd[d]) == "table" and oldd[d][2]
        if not o2 or o2 ~= i[2] then
            self:TriggerEvent("SpecialEvents_UnitDebuffGained", t, i[2], i[3], i[4], i[5])
            if t == "player" then self:TriggerEvent("SpecialEvents_PlayerDebuffGained", i[2], i[3], i[4], i[5]) end
        end
    end

    reclaim(oldb)
    reclaim(oldd, 1)
end


-----------------------------
--      Query Methods      --
-----------------------------

function lib:UnitHasBuff(targ, buff)
    if self.vars.buffs[targ] then return self.vars.buffs[targ][buff] end
end


function lib:UnitHasDebuff(targ, debuff)
    if self.vars.debuffs[targ] then
        for i, v in pairs(self.vars.debuffs[targ]) do
            if v[2] == debuff then
                return v[1], v[3]
            end
        end
    end
end


function lib:UnitHasDebuffType(targ, debufftype)
    if self.vars.debuffs[targ] then
        for i,v in pairs(self.vars.debuffs[targ]) do
            if v[4] == debufftype then return v end
        end
    end
end


function lib:BuffIter(unitid)
    local f = function(unitid, i)
        if not self.vars.buffs[unitid] then return end
        local idx = next(self.vars.buffs[unitid], i)
        local v = self.vars.buffs[unitid][idx]
        if v then return idx, v end
    end
    return f, unitid, nil
end

function lib:DebuffIter(unitid)
    local idx = nil
    local f = function(unitid)
        if not self.vars.debuffs[unitid] then return end
        idx = next(self.vars.debuffs[unitid], idx)
        local v = self.vars.debuffs[unitid][idx]
        if v then return v[2], v[3], v[4], v[5] end
    end
    return f, unitid, nil
end

--------------------------------
--      Load this bitch!      --
--------------------------------
AceLibrary:Register(lib, vmajor, vminor, activate)